乐观锁和悲观锁

场景如下:
    用户账户有余额,当发生交易时,需要实时更新余额。这里如果发生并发问题,那么会造成用户余额和实际交易的不一致,这对公司和客户来说都是很危险的。
那么如何避免,有以下两种方法:
    1、使用悲观锁
            当需要变更余额时,通过代码在事务中对当前需要更新的记录设置for update行锁,然后开始正常的查询和更新操作
            这样,其他的事务只能等待该事务完成后方可操作
            当然要特别注意,如果使用了Spring的事务注解,需要配置一下:
    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->  
    <bean id="transactionManager"  
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource" />  
    </bean>
    <!-- 使用annotation定义事务 -->
     <tx:annotation-driven transaction-manager="transactionManager" />

 在指定代码处添加事务注解

@Transactional
    @Override
    public boolean increaseBalanceByLock(Long userId, BigDecimal amount)
        throws ValidateException {
        long time = System.currentTimeMillis();
        //获取对记录的锁定
        UserBalance balance = userBalanceDao.getLock(userId);
        LOGGER.info("[lock] start. time: {}", time);
        if (null == balance) {
            throw new ValidateException(
                ValidateErrorCode.ERRORCODE_BALANCE_NOTEXIST,
                "user balance is not exist");
        }
        boolean result = userBalanceDao.increaseBalanceByLock(balance, amount);
        long timeEnd = System.currentTimeMillis();
        LOGGER.info("[lock] end. time: {}", timeEnd);
        return result;
    }

通过db的悲观锁,实际测试该方法确实可以有效控制,不过在大并发量的情况下,可能会有性能问题

  <select id="getLock" resultMap="BaseResultMap" parameterType="java.lang.Long">
        <![CDATA[
            select * from user_balance where id=#{id,jdbcType=BIGINT} for update;
        ]]>
    </select>

 

    2、使用乐观锁
            这个方法也同样可以解决场景中描述的问题(我认为比较适合并不频繁的操作):
            设计表的时候增加一个version(版本控制字段),每次需要更新余额的时候,先获取对象,update的时候根据version和id为条件去更新,如果更新回来的数量为0,说明version已经变更
            需要重复一次更新操作,如下:sql脚本
            
update user_balance set Balance = #{balance,jdbcType=DECIMAL},Version = Version+1 where Id = #{id,jdbcType=BIGINT} and Version = #{version,jdbcType=BIGINT}

 这是一种不使用数据库锁的方法,解决方式也很巧妙。当然,在大量并发的情况下,一次扣款需要重复多次的操作才能成功,还是有不足之处的。

 

延伸:

乐观锁:对每次的数据操作都保持乐观的态度,因此不对数据进行上锁。那么就存在数据会被反复读写的情况,所以每次修改数据的时候需要对数据进行判断是否被修改过。

悲观锁:对每次的数据操作持悲观态度,操作时上锁,防止操作时数据被他人修改

使用场景:

乐观锁:由于不上锁,性能较好,适用于读大于写的情况,如果写较多,则会导致重复尝试写入均失败。

悲观锁:上锁,数据写入时会导致读被挂起,适合写大于读的场景

实现:

乐观锁:在db中,可以增加版本号控制,java中的CAS等

悲观锁:db的for update,java中的synchronize等

 

 

posted @ 2016-07-05 15:15  指尖挥舞  阅读(17619)  评论(2编辑  收藏  举报